package tool.lexer

import tool.Reporter
import scala.io.Source
import scala.io.Position

trait Lexer {
    // This indicates to the Scala compiler that this trait will be composed with
    // a Compiler. This implies that the methods from the Reporter class will be
    // available at run-time, as well as the reference to the Source object.
    self: Compiler =>

    import Tokens._

	// list of all the literals used as operators
	val OPERATORS: List[Char] = '&'::'|'::'('::')'::'['::']'::'{'::'}'::':'::';'::'.'::'*'::'/'::'+'::'-'::'<'::'='::'!'::','::Nil

    /* helper functions for character processing */
    def isLetter(c: Char) : Boolean = Character.isLetter(c)
    def isDigit(c: Char) : Boolean = Character.isDigit(c)
    def isDigitNonZero(c: Char) : Boolean = Character.isDigit(c) && !(c == '0')
    def isUnderScore(c: Char) : Boolean = (c == '_')
	def isStringStart(c: Char): Boolean = (c == '"')
    def isValidIdentifierChar(c: Char): Boolean = isUnderScore(c) || isLetter(c) || isDigit(c)
    def isValidIdentifierStart(c: Char): Boolean = isLetter(c)
    def isValidNumericalStart(c: Char): Boolean = isDigit(c)
    def isValidNumerical(c: Char): Boolean = isDigit(c)
	def isValidOperator(c: Char): Boolean = {OPERATORS.foreach(g => if(g == c) return true); return false;}
	def isValidChar(c: Char): Boolean = if(isEol(c)) false else true
	def isWhiteSpace(c: Char): Boolean = Character.isWhitespace(c) || Character.isSpaceChar(c)

	def isEol(c: Char): Boolean = (c == '\n' || c == '\r')

	// the character "buffer"
	var current: String = ""

	// the position of the token being scanned
	var currentPos: Int = 0

	def scanNumber(): Token = {
		var c: Char = next
		do{
			if(isValidNumerical(c)){
				current += c
			}else{
				readChar = c  // push back the invalid character
				readCharDefined = true
				if(current(0) == '0' && current.length > 1){
					error("Integer constant cannot have leading zero", currentPos)
					return Token(BAD)
				}
				return Token(INTLITERAL(current.toInt))
			}
			c = next
		}while(hasNext)

		return Token(INTLITERAL(current.toInt)) //in case we reached the end of file, it shouldn't happen since it's more likely that the lexer
			// will encounter the '\n' character before reaching an EOF char
	}

	def scanString(): Token = {
		var c: Char = next
		var scanning: Boolean = false
		do{
			if(c == '"' && scanning){
				return Token(STRING(current))
			}else if(c == '"'){
				scanning = true
			}else if(isValidChar(c)){
				current += c
			}else{
				error("Invalid caracter in string", source.pos)
				return Token(BAD)
			}
			c = next
		}while(hasNext)

		error("Non-terminated string at end of file", source.pos)
		return Token(BAD) // non-finished string at the end of the file
	}
	

	def getIdentifier(): Token = {
		current match{
			case "if" => Token(IF)
			case "println" => Token(PRINTLN)
			case "while" => Token(WHILE)
			case "else" => Token(ELSE)
			case "return" => Token(RETURN)
			case "var" => Token(VAR)
			case "def" => Token(DEF)
			case "object" => Token(OBJECT)
			case "class" => Token(CLASS)
			case "main" => Token(MAIN)
			case "true" => Token(TRUE)
			case "false" => Token(FALSE)
			case "this" => Token(THIS)
			case "new" => Token(NEW)
			case "length" => Token(ARRAYLENGTH)
			case "Unit" => Token(UNIT)
			case "extends" => Token(EXTENDS)
			case "Int" => Token(INT)
			case "Bool" => Token(BOOL)
			case "String" =>  Token(STRING)

			// exception plugin
			case "try" => Token(TRY)
			case "catch" => Token(CATCH)
            case "throws" => Token(THROWS)
            case "throw" => Token(THROW)
            case "finally" => Token(FINALLY)
			// if we reach this point, it is an identifier
			case _ => Token(ID(current))
		}
	}

	def scanIdentifier(): Token = {
		var c: Char = next
		var t: Token = null
		do{
			if(isValidIdentifierChar(c)){
				current += c
			}else{
				t = getIdentifier()
				readCharDefined = true
				readChar = c
				return t
			}
			c = next
		}while(hasNext)

		return getIdentifier() // shouldn't reach this point, since it will encounter an EOF
	}

	def scanOperator(): Token = {
		var c: Char = next
		var t: Token = null

		do{

			if(current.length > 0){
				current match{
					case "=" if c == '=' => return Token(EQUALS)
					case "=" => 
						readChar = c
						readCharDefined = true
						return Token(ASSIGN)
					case "&" if c == '&' => return Token(AND)
					case "&" =>
						readChar = c
						readCharDefined = true
						error("Invalid operator : &", currentPos)
						return Token(BAD)
					case "|" if c == '|' => return Token(OR)
					case "|" => 
						readChar = c
						readCharDefined = true
						error("Invalid operator : |", currentPos)
						return Token(BAD)
					case "/" if c == '/' =>
						skipLineComment()
						return nextToken
					case "/" if c == '*' => 
						skipBlockComment()
						return nextToken
					case "/" => 
						readChar = c
						readCharDefined = true
						return Token(DIV)
				}
			}else{
				c match{
					case '+' => return Token(PLUS)
					case '-' => return Token(MINUS)
					case '/' => current += c
					case '<' => return Token(LESSTHAN)
					case '!' => return Token(NOT)
					case ':' => return Token(COLON)
					case ';' => return Token(SEMICOLUMN)
					case '(' => return Token(LPAR)
					case ')' => return Token(RPAR)
					case '*' => return Token(TIMES)
					case '[' => return Token(LBRACK)
					case ']' => return Token(RBRACK)
					case '{' => return Token(LCBRACK)
					case '}' =>	return Token(RCBRACK)
					case '.' => return Token(DOT)
					case '=' => current += c
					case '&' => current += c
					case '|' => current += c
					case ',' => return Token(COMA)
					case _ => Token(BAD)
				}
			}

			c = next
		}while(hasNext)

		error("Unexpected end of file", source.pos)
		return Token(BAD)
	}

	def next(): Char = {
		var c: Char = ' '
		if(readCharDefined){
			c = readChar
			readCharDefined = false
		}else if(source.hasNext){
			c = source.next
		}else{
			hasNext = false
		}
		return c
	}

	def skipLineComment() = {
		var lineEnd: Boolean = false
		do{
			val c: Char = next
			if(isEol(c))
				lineEnd = true	
		}while(hasNext && lineEnd == false)

		/* reset current, since it contains a '/' */
		current = ""
	}

	def skipBlockComment() = {
		var foundStar: Boolean = false
		var endOfBlock: Boolean = false
		do{
			if(!source.hasNext){
				fatalError("Unterminated comment at end of file", source.pos)
				endOfBlock = true
			}else{
				val c:Char = next
				if(c == '*')
					foundStar = true
				else if(c == '/' && foundStar)
					endOfBlock = true
				else
					foundStar = false
			}
		}while(endOfBlock == false)

		/* reset current, since it contains a '/' */
		current = ""
	}

	var readChar: Char = ' '
	var readCharDefined: Boolean = false
	var hasNext: Boolean = true

    /** Works like an iterator, and returns the next token from the input stream. */
    def nextToken: Token = {
		var t: Token = null
		var c: Char = ' '

		// skip the whitespaces until we find an interesting character
		do{
			if(!source.hasNext){
				t =  Token(EOF)
				currentPos = source.pos
				t.setPos(source.pos)
				return t
			}
			c = next
		}while(isWhiteSpace(c))

		currentPos = source.pos //get the position of the first char of the token
		readChar = c
		readCharDefined = true

		if(isValidNumericalStart(c)){
			t = scanNumber()
		}else if(isValidOperator(c)){
			t = scanOperator()
		}else if(isValidIdentifierStart(c)){
			t = scanIdentifier()
		}else if(isStringStart(c)){
			t = scanString()
		}else{ 
			t = Token(BAD)
			error("Invalid start of token at :", currentPos)
			readCharDefined = false // we need to set readChar to false, otherwise, the program will enter an ininite loop the next time nextToken is called
		}

		t.setPos(currentPos)
		current = ""
		return t
    }
}
